/* Copyright 2020 The TensorFlow Authors. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/

import {computed, customElement, observe, property} from '@polymer/decorators';
import {html, PolymerElement} from '@polymer/polymer';
import * as PolymerDom from '../../../components/polymer/dom';
import '../../../components/polymer/irons_and_papers';
import {LegacyElementMixin} from '../../../components/polymer/legacy_element_mixin';
import * as tf_graph_render from '../tf_graph_common/render';
import * as tf_graph_scene from '../tf_graph_common/scene';
import * as tf_graph_util from '../tf_graph_common/util';

@customElement('tf-graph-debugger-data-card')
class TfGraphDebuggerDataCard extends LegacyElementMixin(PolymerElement) {
  static readonly template = html`
    <style>
      :host {
        font-size: 12px;
        margin: 0;
        padding: 0;
        display: block;
      }

      h2 {
        padding: 0;
        text-align: center;
        margin: 0;
      }

      .health-pill-legend {
        padding: 15px;
      }

      .health-pill-legend h2 {
        text-align: left;
      }

      .health-pill-entry {
        margin: 10px 10px 10px 0;
      }

      .health-pill-entry .color-preview {
        width: 26px;
        height: 26px;
        border-radius: 3px;
        display: inline-block;
        margin: 0 10px 0 0;
      }

      .health-pill-entry .color-label,
      .health-pill-entry .tensor-count {
        color: #777;
        display: inline-block;
        height: 26px;
        font-size: 22px;
        line-height: 26px;
        vertical-align: top;
      }

      .health-pill-entry .tensor-count {
        float: right;
      }

      #health-pill-step-slider {
        width: 100%;
        margin: 0 0 0 -15px;
        /* 31 comes from adding a padding of 15px from both sides of the paper-slider, subtracting
   * 1px so that the slider width aligns with the image (the last slider marker takes up 1px),
   * and adding 2px to account for a border of 1px on both sides of the image. 30 - 1 + 2.
   * Apparently, the paper-slider lacks a mixin for those padding values. */
        width: calc(100% + 31px);
      }

      #health-pills-loading-spinner {
        width: 20px;
        height: 20px;
        vertical-align: top;
      }

      #health-pill-step-number-input {
        text-align: center;
        vertical-align: top;
      }

      #numeric-alerts-table-container {
        max-height: 400px;
        overflow-x: hidden;
        overflow-y: auto;
      }

      #numeric-alerts-table {
        text-align: left;
      }

      #numeric-alerts-table td {
        vertical-align: top;
      }

      #numeric-alerts-table .first-offense-td {
        display: inline-block;
      }

      .first-offense-td {
        width: 80px;
      }

      .tensor-device-td {
        max-width: 140px;
        word-wrap: break-word;
      }

      .tensor-section-within-table {
        color: #266236;
        cursor: pointer;
        opacity: 0.8;
        text-decoration: underline;
      }

      .tensor-section-within-table:hover {
        opacity: 1;
      }

      .device-section-within-table {
        color: #666;
      }

      .mini-health-pill {
        width: 130px;
      }

      .mini-health-pill > div {
        height: 100%;
        width: 60px;
        border-radius: 3px;
      }

      #event-counts-th {
        padding: 0 0 0 10px;
      }

      .negative-inf-mini-health-pill-section {
        background: rgb(255, 141, 0);
        width: 20px;
      }

      .positive-inf-mini-health-pill-section {
        background: rgb(0, 62, 212);
        width: 20px;
      }

      .nan-mini-health-pill-section {
        background: rgb(204, 47, 44);
        width: 20px;
      }

      .negative-inf-mini-health-pill-section,
      .positive-inf-mini-health-pill-section,
      .nan-mini-health-pill-section {
        color: #fff;
        display: inline-block;
        height: 100%;
        line-height: 20px;
        margin: 0 0 0 10px;
        text-align: center;
      }

      .no-numeric-alerts-notification {
        margin: 0;
      }
    </style>
    <paper-material elevation="1" class="card health-pill-legend">
      <div class="title">
        Enable all (not just sampled) steps. Requires slow disk read.
      </div>
      <paper-toggle-button
        id="enableAllStepsModeToggle"
        checked="{{allStepsModeEnabled}}"
      >
      </paper-toggle-button>
      <h2>
        Step of Health Pills:
        <template is="dom-if" if="[[allStepsModeEnabled]]">
          <input
            type="number"
            id="health-pill-step-number-input"
            min="0"
            max="[[_biggestStepEverSeen]]"
            value="{{specificHealthPillStep::input}}"
          />
        </template>
        <template is="dom-if" if="[[!allStepsModeEnabled]]">
          [[_currentStepDisplayValue]]
        </template>
        <paper-spinner-lite
          active
          hidden$="[[!areHealthPillsLoading]]"
          id="health-pills-loading-spinner"
        ></paper-spinner-lite>
      </h2>
      <template is="dom-if" if="[[allStepsModeEnabled]]">
        <paper-slider
          id="health-pill-step-slider"
          immediate-value="{{specificHealthPillStep}}"
          max="[[_biggestStepEverSeen]]"
          snaps
          step="1"
          value="{{specificHealthPillStep}}"
        ></paper-slider>
      </template>
      <template is="dom-if" if="[[!allStepsModeEnabled]]">
        <template is="dom-if" if="[[_maxStepIndex]]">
          <paper-slider
            id="health-pill-step-slider"
            immediate-value="{{healthPillStepIndex}}"
            max="[[_maxStepIndex]]"
            snaps
            step="1"
            value="{{healthPillStepIndex}}"
          ></paper-slider>
        </template>
      </template>
      <h2>
        Health Pill
        <template is="dom-if" if="[[healthPillValuesForSelectedNode]]">
          Counts for Selected Node
        </template>
        <template is="dom-if" if="[[!healthPillValuesForSelectedNode]]">
          Legend
        </template>
      </h2>
      <template is="dom-repeat" items="[[healthPillEntries]]">
        <div class="health-pill-entry">
          <div
            class="color-preview"
            style="background:[[item.background_color]]"
          ></div>
          <div class="color-label">[[item.label]]</div>
          <div class="tensor-count">
            [[_computeTensorCountString(healthPillValuesForSelectedNode,
            index)]]
          </div>
        </div>
      </template>
      <div hidden$="[[!_hasDebuggerNumericAlerts(debuggerNumericAlerts)]]">
        <h2 id="numeric-alerts-header">Numeric Alerts</h2>
        <p>Alerts are sorted from top to bottom by increasing timestamp.</p>
        <div id="numeric-alerts-table-container">
          <table id="numeric-alerts-table">
            <thead>
              <tr>
                <th>First Offense</th>
                <th>Tensor (Device)</th>
                <th id="event-counts-th">Event Counts</th>
              </tr>
            </thead>
            <tbody id="numeric-alerts-body"></tbody>
          </table>
        </div>
      </div>
      <template
        is="dom-if"
        if="[[!_hasDebuggerNumericAlerts(debuggerNumericAlerts)]]"
      >
        <p class="no-numeric-alerts-notification">
          No numeric alerts so far. That is likely good. Alerts indicate the
          presence of NaN or (+/-) Infinity values, which may be concerning.
        </p>
      </template>
    </paper-material>
  `;
  @property({type: Object})
  renderHierarchy: tf_graph_render.RenderGraphInfo;
  @property({
    type: Array,
    notify: true,
  })
  debuggerNumericAlerts: any;
  @property({type: Object})
  nodeNamesToHealthPills: any;
  @property({
    type: Number,
    notify: true,
  })
  healthPillStepIndex: any;
  // Only relevant if we are in all steps mode, in which case the user may want to view health
  // pills for a specific step.
  @property({
    type: Number,
    notify: true,
  })
  specificHealthPillStep: number = 0;
  // Two-ways
  @property({
    type: String,
    notify: true,
  })
  selectedNode: any;
  @property({
    type: String,
    notify: true,
  })
  highlightedNode: any;
  // The enum value of the include property of the selected node.
  @property({
    type: Number,
    notify: true,
  })
  selectedNodeInclude: any;
  // Whether health pills are currently being loaded, in which case we show a spinner (and the
  // current health pills shown might be out of date).
  @property({type: Boolean})
  areHealthPillsLoading: any;
  @property({
    type: Array,
  })
  healthPillEntries: unknown[] = tf_graph_scene.healthPillEntries;
  // When all-steps mode is enabled, the user can request health pills for any step. In this
  // mode, Tensorboard makes a request every time the user drags the slider to a different step.
  @property({
    type: Boolean,
    notify: true,
  })
  allStepsModeEnabled: any;
  ready() {
    super.ready();
    var mainContainer = document.getElementById('mainContainer');
    var scrollbarContainer = document.querySelector(
      'tf-dashboard-layout .scrollbar'
    ) as HTMLElement | null;
    if (mainContainer && scrollbarContainer) {
      // If this component is being used inside of TensorBoard's dashboard layout, it may easily
      // cause the dashboard layout element to overflow, giving the user 2 scroll bars. Prevent
      // that by hiding whatever content overflows - the user will have to expand the viewport to
      // use this debugging card.
      mainContainer.style.overflow = 'hidden';
      scrollbarContainer.style.overflow = 'hidden';
    }
  }
  _healthPillsAvailable(debuggerDataEnabled: any, nodeNamesToHealthPills: any) {
    // So long as there is a mapping (even if empty) from node name to health pills, show the
    // legend and slider. We do that because, even if no health pills exist at the current step,
    // the user may desire to change steps, and the slider must show for the user to do that.
    return debuggerDataEnabled && nodeNamesToHealthPills;
  }
  _computeTensorCountString(
    healthPillValuesForSelectedNode: any,
    valueIndex: any
  ) {
    if (!healthPillValuesForSelectedNode) {
      // No health pill data is available.
      return '';
    }
    return healthPillValuesForSelectedNode[valueIndex].toFixed(0);
  }
  @computed(
    'nodeNamesToHealthPills',
    'healthPillStepIndex',
    'selectedNode',
    'allStepsModeEnabled',
    'areHealthPillsLoading'
  )
  get healthPillValuesForSelectedNode(): unknown[] | null {
    var nodeNamesToHealthPills = this.nodeNamesToHealthPills;
    var healthPillStepIndex = this.healthPillStepIndex;
    var selectedNode = this.selectedNode;
    var allStepsModeEnabled = this.allStepsModeEnabled;
    var areHealthPillsLoading = this.areHealthPillsLoading;
    if (areHealthPillsLoading) {
      // Health pills are loading. Do not render data that is out of date.
      return null;
    }
    if (!selectedNode) {
      // No node is selected.
      return null;
    }
    const healthPills = nodeNamesToHealthPills[selectedNode];
    if (!healthPills) {
      // This node lacks a health pill.
      return null;
    }
    // If all steps mode is enabled, we use the first health pill in the list because the JSON
    // response from the server is a mapping between node name and a list of 1 health pill.
    const healthPill =
      healthPills[allStepsModeEnabled ? 0 : healthPillStepIndex];
    if (!healthPill) {
      // This node lacks a health pill at the current step.
      return null;
    }
    // The health pill count values start at 2. Each health pill contains 6 values.
    return healthPill.value.slice(2, 8);
  }
  @computed(
    'nodeNamesToHealthPills',
    'healthPillStepIndex',
    'allStepsModeEnabled',
    'specificHealthPillStep',
    'areHealthPillsLoading'
  )
  get _currentStepDisplayValue(): any {
    var nodeNamesToHealthPills = this.nodeNamesToHealthPills;
    var healthPillStepIndex = this.healthPillStepIndex;
    var allStepsModeEnabled = this.allStepsModeEnabled;
    var specificHealthPillStep = this.specificHealthPillStep;
    var areHealthPillsLoading = this.areHealthPillsLoading;
    if (allStepsModeEnabled) {
      // The user seeks health pills for specific step from the server.
      return specificHealthPillStep.toFixed(0);
    }
    if (areHealthPillsLoading) {
      // The current step is undefined.
      return 0;
    }
    for (let nodeName in nodeNamesToHealthPills) {
      // All nodes have the same number of steps stored, so only examine 1 node. We cannot
      // directly index into the nodeNamesToHealthPills object because we do not have a key.
      // If all steps mode is enabled, we only have 1 step to show.
      return nodeNamesToHealthPills[nodeName][healthPillStepIndex].step.toFixed(
        0
      );
    }
    // The current step could not be computed.
    return 0;
  }
  // The biggest step value ever seen. Used to determine what steps of health pills to let the
  // user fetch in all steps mode.
  @computed('nodeNamesToHealthPills')
  get _biggestStepEverSeen(): number {
    var nodeNamesToHealthPills = this.nodeNamesToHealthPills;
    for (let nodeName in nodeNamesToHealthPills) {
      // All nodes have the same number of steps stored, so only examine 1 node.
      // The index is 1 less than the count. Tensorboard backend logic guarantees that the length
      // of the array will be greater than 1.
      var healthPills = nodeNamesToHealthPills[nodeName];
      return Math.max(
        this._biggestStepEverSeen,
        healthPills[healthPills.length - 1].step
      );
    }
    // No steps seen so far. Default to 0.
    return this._biggestStepEverSeen || 0;
  }
  @computed('nodeNamesToHealthPills')
  get _maxStepIndex(): number {
    var nodeNamesToHealthPills = this.nodeNamesToHealthPills;
    for (let nodeName in nodeNamesToHealthPills) {
      // All nodes have the same number of steps stored, so only examine 1 node.
      // The index is 1 less than the count. Tensorboard backend logic guarantees that the length
      // of the array will be greater than 1.
      return nodeNamesToHealthPills[nodeName].length - 1;
    }
    // Return a falsy value. The slider should be hidden.
    return 0;
  }
  _hasDebuggerNumericAlerts(debuggerNumericAlerts: any) {
    return debuggerNumericAlerts && debuggerNumericAlerts.length;
  }
  @observe('debuggerNumericAlerts')
  _updateAlertsList() {
    var debuggerNumericAlerts = this.debuggerNumericAlerts;
    var alertBody = this.$$('#numeric-alerts-body');
    if (!alertBody) {
      return;
    }
    (alertBody as HTMLElement).innerText = '';
    for (var i = 0; i < debuggerNumericAlerts.length; i++) {
      var alert = debuggerNumericAlerts[i];
      var tableRow = document.createElement('tr');
      var timestampTd = document.createElement('td');
      timestampTd.innerText = tf_graph_util.computeHumanFriendlyTime(
        alert.first_timestamp
      );
      timestampTd.classList.add('first-offense-td');
      tableRow.appendChild(timestampTd);
      var tensorDeviceTd = document.createElement('td');
      tensorDeviceTd.classList.add('tensor-device-td');
      var tensorSection = document.createElement('div');
      tensorSection.classList.add('tensor-section-within-table');
      tensorSection.innerText = alert.tensor_name;
      this._addOpExpansionListener(tensorSection, alert.tensor_name);
      tensorDeviceTd.appendChild(tensorSection);
      var deviceSection = document.createElement('div');
      deviceSection.classList.add('device-section-within-table');
      deviceSection.innerText = '(' + alert.device_name + ')';
      tensorDeviceTd.appendChild(deviceSection);
      tableRow.appendChild(tensorDeviceTd);
      var miniHealthPill = document.createElement('div');
      miniHealthPill.classList.add('mini-health-pill');
      var miniHealthPillTd = document.createElement('td');
      miniHealthPillTd.classList.add('mini-health-pill-td');
      miniHealthPillTd.appendChild(miniHealthPill);
      tableRow.appendChild(miniHealthPillTd);
      if (alert.neg_inf_event_count) {
        var negativeInfCountSection = document.createElement('div');
        negativeInfCountSection.classList.add(
          'negative-inf-mini-health-pill-section'
        );
        negativeInfCountSection.innerText = alert.neg_inf_event_count;
        negativeInfCountSection.setAttribute(
          'title',
          alert.neg_inf_event_count + ' events with -\u221E'
        );
        miniHealthPill.appendChild(negativeInfCountSection);
      }
      if (alert.pos_inf_event_count) {
        var positiveInfCountSection = document.createElement('div');
        positiveInfCountSection.classList.add(
          'positive-inf-mini-health-pill-section'
        );
        positiveInfCountSection.innerText = alert.pos_inf_event_count;
        positiveInfCountSection.setAttribute(
          'title',
          alert.pos_inf_event_count + ' events with +\u221E'
        );
        miniHealthPill.appendChild(positiveInfCountSection);
      }
      if (alert.nan_event_count) {
        var nanCountSection = document.createElement('div');
        nanCountSection.classList.add('nan-mini-health-pill-section');
        nanCountSection.innerText = alert.nan_event_count;
        nanCountSection.setAttribute(
          'title',
          alert.nan_event_count + ' events with NaN'
        );
        miniHealthPill.appendChild(nanCountSection);
      }
      (PolymerDom.dom(alertBody) as any).appendChild(tableRow);
    }
  }
  // Adds a listener to an element, so that when that element is clicked, the tensor with
  // tensorName expands.
  _addOpExpansionListener(clickableElement: any, tensorName: any) {
    clickableElement.addEventListener('click', () => {
      // When the user clicks on a tensor name, expand all nodes until the user can see the
      // associated node.
      var nameOfNodeToSelect = tf_graph_render.expandUntilNodeIsShown(
        document.getElementById('scene'),
        this.renderHierarchy,
        tensorName
      );
      // Store the current scroll of the graph info card. Node selection alters that scroll, and
      // we restore the scroll later.
      var previousScrollFromBottom: any;
      var graphInfoCard = document.querySelector(
        'tf-graph-info#graph-info'
      ) as any;
      if (graphInfoCard) {
        previousScrollFromBottom =
          graphInfoCard.scrollHeight - graphInfoCard.scrollTop;
      }
      // Update the selected node within graph logic.
      var previousSelectedNode = this.selectedNode;
      this.set('selectedNode', nameOfNodeToSelect);
      // Scroll the graph info card back down if necessary so that user can see the alerts section
      // again. Selecting the node causes the info card to scroll to the top, which may mean the
      // user no longer sees the list of alerts.
      var scrollToOriginalLocation = () => {
        graphInfoCard.scrollTop =
          graphInfoCard.scrollHeight - previousScrollFromBottom;
      };
      if (graphInfoCard) {
        // This component is used within an info card. Restore the original scroll.
        if (previousSelectedNode) {
          // The card for the selected node has already opened. Immediately restore the scroll.
          scrollToOriginalLocation();
        } else {
          // Give some time for the DOM of the info card to be created before scrolling down.
          window.setTimeout(scrollToOriginalLocation, 20);
        }
      }
    });
  }
}
